﻿/*
Copyright (c) 2007-2011 William POTTIER - AllProgrammic

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights 
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 
of the Software, and to permit persons to whom the Software is furnished to do so, 
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all 
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using NetAccess.Common;
using NetAccess.Common.Exceptions;
using NetAccess.Formatters;
using NetAccess.Message;
using System.Net.NetworkInformation;

namespace NetAccess.Client
{
    public partial class ClientLayer
    {
        private static Socket _serviceDiscoverySocket;
        private static ManualResetEvent _serviceDiscoveryWaitReceive;
        private static bool _isServiceDiscoveryCurrentlyActive;
        private static ClientConfiguration _serviceDiscoveryConfiguration;
        private static Action<ServiceData> _serviceDiscoveryNewServerCallback;
        private static Action _serviceDiscoveryEndDiscoveryCallback;

        /// <summary>
        /// Start the services discovery
        /// </summary>
        /// <param name="config"></param>
        /// <param name="useIPv6Multicast"></param>
        /// <param name="newServercallback"></param>
        /// <param name="endDiscoverycallback"></param>
        public static void BeginDiscovery(ClientConfiguration config, bool useIPv6Multicast, Action<ServiceData> newServercallback, Action endDiscoverycallback)
        {
            if (_isServiceDiscoveryCurrentlyActive)
            {
                return;
            }

            _isServiceDiscoveryCurrentlyActive = true;

            // Verouille la configuration
            _serviceDiscoveryConfiguration = (ClientConfiguration)config.Clone();
            _serviceDiscoveryConfiguration.LockConfiguration();
            
            // Mémorise le callback à appelé quand un nouveau serveur est découvert
            _serviceDiscoveryNewServerCallback = newServercallback;
            _serviceDiscoveryEndDiscoveryCallback = endDiscoverycallback;

            // Création du socket
            if (useIPv6Multicast)
            {
                _serviceDiscoverySocket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);

                var multicastOption = new IPv6MulticastOption(IPAddress.Parse("ff02::1"));
                _serviceDiscoverySocket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.AddMembership, multicastOption);
            }
            else
            {
                _serviceDiscoverySocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
                                              {EnableBroadcast = true};
            }


            if (config.SendBufferSize > 0)
                _serviceDiscoverySocket.SendBufferSize = config.SendBufferSize;
            if (config.ReceiveBufferSize > 0)
                _serviceDiscoverySocket.ReceiveBufferSize = config.ReceiveBufferSize;
            _serviceDiscoverySocket.Bind(new IPEndPoint(IPAddress.Any, 0));

            // Crée le packet de découverte
            var message = new InternalMessage(InternalMessageIdentifier.DiscoverServices, _serviceDiscoveryConfiguration.ApplicationIdentifier);

            // Format de message
            var formatter = new MessageFormatter(_serviceDiscoverySocket.SendBufferSize);

            // Message sous forme binaire
            var datas = formatter.Prepare(message);

            // Début de la mise en écoute des retours
            ThreadPool.QueueUserWorkItem(ListenServiceDiscoveryIncommingData);

            // Envoi du packet de broadcast
            var broadcastIPs = GetIpv4AddressToBroadcast();
            broadcastIPs.Add(IPAddress.Loopback);

            foreach (var ip in broadcastIPs)
            {
                var ep = new IPEndPoint(ip, _serviceDiscoveryConfiguration.ServiceDiscoveryPort);

                var sendQueue = new Queue<byte[]>(datas);
                while (sendQueue.Count > 0)
                {
                    byte[] buffer = sendQueue.Dequeue();

                    try
                    {
                        _serviceDiscoverySocket.SendTo(buffer, 0, buffer.Length, SocketFlags.None, ep);
                    }
                    catch (SocketException se)
                    {
                        throw new NetAccessException(String.Format("Unable to discover service. See InnerException for more informations. (Endpoint : {0})", ep), se);
                    }
                }
            }
        }

        /// <summary>
        /// Stop the services discovery
        /// </summary>
        public static void StopDiscovery()
        {
            if (_isServiceDiscoveryCurrentlyActive)
            {
                if (_serviceDiscoverySocket != null)
                    _serviceDiscoverySocket.Close();
                _serviceDiscoverySocket = null;

                if (_serviceDiscoveryWaitReceive != null)
                {
                    _serviceDiscoveryWaitReceive.Set();
                    _serviceDiscoveryWaitReceive.Close();
                }

                _serviceDiscoveryWaitReceive = null;
            }
        }

        private static void ListenServiceDiscoveryIncommingData(object state)
        {
            Thread.CurrentThread.Name = "ListenServiceDiscoveryIncommingData";

            _serviceDiscoveryWaitReceive = new ManualResetEvent(false);

            EndPoint remote = new IPEndPoint(IPAddress.Any, 0);

            // Start timeout timer.
            var serviceDiscoveryTimeoutTimer = new Timer(ServiceDiscoveryTimeout, null, _serviceDiscoveryConfiguration.DiscoverTimeout, TimeSpan.FromMilliseconds(-1));

            while (_isServiceDiscoveryCurrentlyActive)
            {
                try
                {
                    _serviceDiscoveryWaitReceive.Reset();
                }
                catch (NullReferenceException) { break; }
                catch (ObjectDisposedException) { break; }

                try
                {
                    var buffer = new byte[_serviceDiscoverySocket.ReceiveBufferSize];
                    _serviceDiscoverySocket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref remote, CallbackServiceDiscoveryIncommingData, buffer);
                }
                catch (SocketException se)
                {
                    switch (se.SocketErrorCode)
                    {
                        case SocketError.ConnectionReset:
                            Logger.Instance.Debug(se);
                            break;
                        default:
                            Logger.Instance.Error(se);
                            break;
                    }
                }
                catch (ObjectDisposedException) { break; }
                catch (NullReferenceException) { break; }

                try
                {
                    _serviceDiscoveryWaitReceive.WaitOne();
                }
                catch (NullReferenceException) { break; }
                catch (ObjectDisposedException) { break; }
            }

            serviceDiscoveryTimeoutTimer.Dispose();

            _serviceDiscoveryEndDiscoveryCallback.Invoke();
            _isServiceDiscoveryCurrentlyActive = false;
        }

        private static void ServiceDiscoveryTimeout(object state)
        {
            if (_isServiceDiscoveryCurrentlyActive)
            {
                if (_serviceDiscoverySocket != null)
                    _serviceDiscoverySocket.Close();
                _serviceDiscoverySocket = null;

                if (_serviceDiscoveryWaitReceive != null)
                {
                    _serviceDiscoveryWaitReceive.Set();
                    _serviceDiscoveryWaitReceive.Close();
                }

                _serviceDiscoveryWaitReceive = null;
            }
        }

        private static void CallbackServiceDiscoveryIncommingData(IAsyncResult ar)
        {
            var buffer = (byte[])ar.AsyncState;
            EndPoint remote = new IPEndPoint(IPAddress.None, 0);
            int numread = 0;

            try
            {
                numread = _serviceDiscoverySocket.EndReceiveFrom(ar, ref remote);
            }
            catch (ObjectDisposedException) { return; }
            catch (NullReferenceException) { return; }
            catch (SocketException se)
            {
                if (se.SocketErrorCode == SocketError.ConnectionReset) 
                {
                    // Error throw by UDP stack if no remote service available;
                    // This is not an error during the service discovery
                    _serviceDiscoveryWaitReceive.Set();
                    return;
                }
                Logger.Instance.Error(se);
            }

            try
            {
                _serviceDiscoveryWaitReceive.Set();
            }
            catch (ObjectDisposedException) { }
            catch (NullReferenceException) { }

            if (numread > 0)
            {
                var ms = new MemoryStream(buffer, 0, numread);
                var formatter = new MessageFormatter(_serviceDiscoverySocket.ReceiveBufferSize);
                formatter.AddDataToReception(ms.ToArray(), numread);
                Queue<InternalMessage> messages = formatter.ProcessIncommingMessages();
                while (messages.Count > 0)
                {
                    InternalMessage message = messages.Dequeue();
                    if (message.MessageIdentifier == InternalMessageIdentifier.DiscoverServices)
                    {
                        ServiceData data = null;

                        try
                        {
                            var appId = _serviceDiscoveryConfiguration.ApplicationIdentifier;
                            var ipAddress = ((IPEndPoint)remote).Address;
                            var tcpPort = (int)message.MessageData[1];
                            var udpPort = (int)message.MessageData[2];
                            var serviceName = (string)message.MessageData[0];
                            var useCompression = (bool)message.MessageData[3];
                            var useCrypt = (bool)message.MessageData[4];
                            var publicKey = (string)message.MessageData[5];
                            var nbClient = (int?)message.MessageData[6];
                            var availableSlots = (int?)message.MessageData[7];

                            data = new ServiceData(
                                appId,
                                ipAddress,
                                tcpPort,
                                udpPort,
                                serviceName,
                                useCompression,
                                useCrypt,
                                publicKey,
                                nbClient,
                                availableSlots
                                );
                        }
                        catch (Exception ex)
                        {
                            Logger.Instance.Warning(new NetAccessException("Receive a missformed discover message.", ex));
                        }
                        if (data != null)
                        {
                            try
                            {
                                _serviceDiscoveryNewServerCallback.Invoke(data);
                            }
                            catch (Exception ex)
                            {
                                Logger.Instance.Error(new NetAccessException("Discover callback throw an exception. Please consult inner exception.", ex));
                            }
                        }
                    }
                }
            }
        }

        private static IList<IPAddress> GetIpv4AddressToBroadcast()
        {
            IEnumerable<NetworkInterface> nics = NetworkInterface.GetAllNetworkInterfaces().Where(ni => ni.OperationalStatus == OperationalStatus.Up && ni.NetworkInterfaceType != NetworkInterfaceType.Loopback);
            List<IPAddress> broadcast = new List<IPAddress>();

            foreach (NetworkInterface adapter in nics)
            {
                var unicasts = adapter.GetIPProperties().UnicastAddresses.Where(ip => ip.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork);
                foreach (var ip in unicasts)
                {
                    broadcast.Add(ip.Address.GetBroadcastAddress(ip.IPv4Mask));
                }
            }

            return broadcast;
        }
    }
}
